Lektion 27



Willkommen zu einem ziemlich komplexen Tutorial über Schatten-werfen. Der Effekt, den dieses Demo erzeugt ist einfach unglaublich. Schatten die langgezogen, gekrümmt und gebrochen an anderen Objekten oder entlägs von Mauern verlaufen. Alles in der Szene kann mittels der Tasten auf der Tastatur im 3D Raum bewegt werden.

Dieses Tutorial wählt diesmal einen etwas anderen Ansatz - es wird davon ausgegangen, dass Sie bereits gute OpenGL Kentnisse haben. Sie sollten den Stencil Buffer verstehen und das grundlegende OpenGL Setup. Wenn Sie eine Auffrischung benötigen, gehen Sie zurück und lesen Sie die vorherigen Tutorials. Funktionen wie CreateGLWindow und WinMain werden NICHT in diesem Tutorial erklärt. Außerdem wird fundamentale 3D Mathematik vorausgesetzt, halten Sie also ein Nachschlagewerk bereit! (Ich habe meine Aufzeichnungen des ersten Semesters aus der Uni genommen - Ich wußte, dass dieser mal später nützlich sein werden! :)

Als erstes haben wir die Defintion von INFINITY (=unendlich), welche beschreibt, wie weit sich die "Schatten-Volumen-Polygone" ausbreiten (das wird später noch erklärt). Wenn Sie ein größeres oder kleineres Koordinaten-System verwenden, passen Sie diesen Wert entsprechend an.

// Definition von "INFINITY" um den Ausbreitungs-Vektor für das Schatten-Volumen zu berechnen
#define INFINITY	100

Als nächstes folgt die Definition der Objekt-Strukturen.

Die Point3f Struktur enthält eine Koordinate im 3D Raum. Diese kann für Vertices oder Vekotren verwendet werden.

// Struktur die ein Vertex in einem Objekt beschreibt
struct Point3f
{
	GLfloat x, y, z;
};

Die Plane-Struktur enthält 4 Werte, die die Gleichung einer Ebene darstellen. Diese Ebenen werden die Seiten eines Objektes darstellen.

// Struktur die eine Ebene beschreibt, in dem Format: ax + by + cz + d = 0
struct Plane
{
	GLfloat a, b, c, d;
};

Die Face Struktur enthält alle Informationen, die über eine Dreieck benötigt werden, um einen Schatten zu werfen.

// Struktur die eine Objekt-Seite beschreibt
struct Face
{
	int vertexIndices[3];			// Index eines jeden Vertex innerhalb eines Objekts, dass das Dreieck dieser Seite bildet
	Point3f normals[3];			// Normalenvektor für jeden Vertex
	Plane planeEquation;			// Gleichung einer Ebene, in der das Dreieck liegt
	int neighbourIndices[3];		// Index einer jeden Seite, die mit diesem in dem Objekt benachbart ist
	bool visible;				// Ist die Seite für das Licht sichtbar?
};

Letzlich enthält die ShadowedObject Struktur all die Vertices und Seiten aus dem Objekt. Der Speicher für jedes Array wird dynamisch erzeugt, wenn sie geladen wird.

struct ShadowedObject
{
	int nVertices;
	Point3f *pVertices;			// wird dynamisch alloziiert

	int nFaces;
	Face *pFaces;				// wird dynamisch alloziiert
};

Die readObject Funktion ist ziemlich selbsterklärend. Sie füllt die gegebenen Strukturen mit Werten, die aus einer Datei gelesen werden, alloziiert Speicher für die Vertices und für die Seiten. Sie initialisiert außerdem die Nachbarn auf -1, was bedeutet, dass es (bis jetzt) keine gibt. Diese werden später berechnet.

bool readObject( const char *filename, ShadowedObject& object )
{
	FILE *pInputFile;
	int i;

	pInputFile = fopen( filename, "r" );
	if ( pInputFile == NULL )
	{
		cerr << "Unable to open the object file: " << filename << endl;
		return false;
	}

	// lese Vertices
	fscanf( pInputFile, "%d", &object.nVertices );
	object.pVertices = new Point3f[object.nVertices];
	for ( i = 0; i < object.nVertices; i++ )
	{
		fscanf( pInputFile, "%f", &object.pVertices[i].x );
		fscanf( pInputFile, "%f", &object.pVertices[i].y );
		fscanf( pInputFile, "%f", &object.pVertices[i].z );
	}

	// lese Seiten
	fscanf( pInputFile, "%d", &object.nFaces );
	object.pFaces = new Face[object.nFaces];
	for ( i = 0; i < object.nFaces; i++ )
	{
		int j;
		Face *pFace = &object.pFaces[i];

		for ( j = 0; j < 3; j++ )
			pFace->neighbourIndices[j] = -1;	// es gibt noch keine Nachbarn

		for ( j = 0; j < 3; j++ )
		{
			fscanf( pInputFile, "%d", &pFace->vertexIndices[j] );
			pFace->vertexIndices[j]--;		// Dateien geben Sie in einem Array basierend auf 1 an, aber wir benutzen ein Array das mit 0 beginnt
		}

		for ( j = 0; j < 3; j++ )
		{
			fscanf( pInputFile, "%f", &pFace->normals[j].x );
			fscanf( pInputFile, "%f", &pFace->normals[j].y );
			fscanf( pInputFile, "%f", &pFace->normals[j].z );
		}
	}
	return true;
}

Genauso ist killObjekt selbsterklärend - es werden einfach alle dynamisch alloziierten Arrays innerhalb des Objekts gelöscht, wenn Sie mit diesen fertig sind. Beachten Sie, dass eine Zeile zu KillGLWindow hinzugefügt wurde, um diese Funktion für die betreffenden Objekte aufzurufen.

void killObject( ShadowedObject& object )
{
	delete[] object.pFaces;
	object.pFaces = NULL;
	object.nFaces = 0;

	delete[] object.pVertices;
	object.pVertices = NULL;
	object.nVertices = 0;
}

Nun, mit setConnectivity wird es interessant. Diese Funktion wird verwendet, um herauszufinden, welche Nachbarn jede Seite in dem gegebenen Objekt hat. Hier etwas Pseudo-Code:

für jede Seite (A) in dem Objekt
        für jede Kante in A
                wenn wir den Kantennachbar noch nicht kennen
                        für jede Seite (B) in dem Objekt (außer A)
                                für jede Kante in B
                                        wenn A's Kante die selbe wie B's
 Kante ist, dann sind diese über diese Kante benachbart
                                                setze die 
Nachbars-Eigenschaft für jede Seite A und B, dann fahre mit der nächsten
 Kante in A fort

Die letzten beiden Zeilen werden mit folgendem Code verwirklicht. Indem die zwei Vertices gefunden werden, die das Ende der Kante markieren und diese verglichen werden, können Sie feststellen, ob es die selbe Kante ist. Der Teil (edgeA+1)%3 ermittelt einen Vertex, der neben dem liegt, den Sie betrachten. Dann überprüfen Sie, ob die Vertices übereinstimmen (die Reihenfolge mag differieren, abhängig vom zweiten Fall des if Statements).

	int vertA1 = pFaceA->vertexIndices[edgeA];
	int vertA2 = pFaceA->vertexIndices[( edgeA+1 )%3];

	int vertB1 = pFaceB->vertexIndices[edgeB];
	int vertB2 = pFaceB->vertexIndices[( edgeB+1 )%3];

	// überprüfe ob es Nachbarn gibt - z.B. Die Kanten sind die Selben
	if (( vertA1 == vertB1 && vertA2 == vertB2 ) || ( vertA1 == vertB2 && vertA2 == vertB1 ))
	{
		pFaceA->neighbourIndices[edgeA] = faceB;
		pFaceB->neighbourIndices[edgeB] = faceA;
		edgeFound = true;
		break;
	}

Glücklicherweise eine andere einfach Funktion, während Sie erst mal Luft holen können. drawObject rendert jede Seite, eine nach der anderen.

// Zeichne ein Objekt - Zeichne einfach jede Dreiecks-Seite.
void drawObject( const ShadowedObject& object )
{
	glBegin( GL_TRIANGLES );
	for ( int i = 0; i < object.nFaces; i++ )
	{
		const Face& face = object.pFaces[i];

		for ( int j = 0; j < 3; j++ )
		{
			const Point3f& vertex = object.pVertices[face.vertexIndices[j]];

			glNormal3f( face.normals[j].x, face.normals[j].y, face.normals[j].z );
			glVertex3f( vertex.x, vertex.y, vertex.z );
		}
	}
	glEnd();
}
 

Die Berechnung der Ebenen-Gleichung sieht ziemlich hässlich aus, aber es ist nur eine einfache mathematische Formel, die Sie in jedem Mathebuch nachschlagen können, wenn Sie sie benötigen.

void calculatePlane( const ShadowedObject& object, Face& face )
{
	// ermittle verkürzte Namen für die Vertices für jede Seite
	const Point3f& v1 = object.pVertices[face.vertexIndices[0]];
	const Point3f& v2 = object.pVertices[face.vertexIndices[1]];
	const Point3f& v3 = object.pVertices[face.vertexIndices[2]];

	face.planeEquation.a = v1.y*(v2.z-v3.z) + v2.y*(v3.z-v1.z) + v3.y*(v1.z-v2.z);
	face.planeEquation.b = v1.z*(v2.x-v3.x) + v2.z*(v3.x-v1.x) + v3.z*(v1.x-v2.x);
	face.planeEquation.c = v1.x*(v2.y-v3.y) + v2.x*(v3.y-v1.y) + v3.x*(v1.y-v2.y);
	face.planeEquation.d = -( v1.x*( v2.y*v3.z - v3.y*v2.z ) +
				v2.x*(v3.y*v1.z - v1.y*v3.z) +
				v3.x*(v1.y*v2.z - v2.y*v1.z) );
}

Haben Sie bereits einmal tief Luft geholt? Gut, da Sie jetzt lernen werden, wie man einen Schatten wirft! Die castShadow Funktion enthält all den GL spezifischen Kram und übergibt diesen doShadowPass, um den Schatten in zwei Durchgängen zu rendern.

Als erstes bestimmen wir, welche Fläche zum Licht gerichtet ist. Wir machen das, indem wir nachschauen, auf welche Ebene das Licht scheint. Das wird dadurch erreicht, dass wir die Lichtposition in die Ebene für die Gleichung einsetzen. Wenn diese größer als 0 ist, dann ist diese in der selben Richtung wie der Normalenvektor zur Ebene und sichbar für das Licht. Wenn nicht, dann ist sie nicht sichtbar für das Licht (Erneut: schauen Sie in einem guten Mathebuch für 3D-Geometrie nach, für eine bessere Erklärung).

void castShadow( ShadowedObject& object, GLfloat *lightPosition )
{
	// bestimme, welche Seiten für das Licht sichtbar sind.
	for ( int i = 0; i < object.nFaces; i++ )
	{
		const Plane& plane = object.pFaces[i].planeEquation;

		GLfloat side = plane.a*lightPosition[0]+
			plane.b*lightPosition[1]+
			plane.c*lightPosition[2]+
			plane.d;

		if ( side > 0 )
			object.pFaces[i].visible = true;
		else
			object.pFaces[i].visible = false;
	}

Der nächste Abschnitt setzt die nötigen OpenGL Status um Schatten zu rendern.

Als erstes pushen wir alle Attribute auf den Stack, die modifiziert werden. Das macht das Wiederherstellen um ein vielfaches leichter.

Beleuchtung ist deaktiviert, da wir nicht in den Farb (Ausgabe) Buffer rendern werden, nur in den Stencil Buffer. Aus dem selben Grund schaltet die Farb-Maske alle Farb-Komponenten aus (weshalb das Zeichnen eines Polygons nicht bis zum Ausgabe Buffer gelangen würde).

Obwohl Depth Testing immer noch benutzt wird, wollen wir die Schatten nicht als solide Objekte im Depth Buffer erscheinen lassen, weshalb die Depth Maske das verhindert.

Der Stencil Buffer wird eingeschaltet, weil in diesen die Schatten gezeichnet werden.

	glPushAttrib( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_ENABLE_BIT | GL_POLYGON_BIT | GL_STENCIL_BUFFER_BIT );
	glDisable( GL_LIGHTING );					// schalte Beleuchtung aus
	glDepthMask( GL_FALSE );					// schalte Schreibzugriffe auf den Depth-Buffer aus
	glDepthFunc( GL_LEQUAL );
	glEnable( GL_STENCIL_TEST );					// schalte Stencil Buffer Testing ein
	glColorMask( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );		// zeichne nicht in den Colour Buffer
	glStencilFunc( GL_ALWAYS, 1, 0xFFFFFFFFL );

Ok, nun werden die Schatten tatsächlich gerendert. Wir kommen darauf gleich zurück, wenn wir uns die doShadowPass Funktion anschauen. Diese werden in zwei Durchgängen gerendert, wie Sie sehen können, einer inkrementiert den Stencil Buffer mit der Vorder-Seite (welche den Schatten wirft), der zweite dekrementiert den Stencil Buffer mit der Hinter-Seite ("ausschalten" des Schattens zwischen dem Objekt und jeder anderen Oberfläche).

	// Erster Durchgang. Inkrementiere Stencil Wert im Schatten.
	glFrontFace( GL_CCW );
	glStencilOp( GL_KEEP, GL_KEEP, GL_INCR );
	doShadowPass( object, lightPosition );
	// Zweiter Durchgang. Dekrementiere Stencil Wert im Schatten,
	glFrontFace( GL_CW );
	glStencilOp( GL_KEEP, GL_KEEP, GL_DECR );
	doShadowPass( object, lightPosition );

Um den zweiten Durchgang zu verstehen, kommentiere Sie diesen am besten aus und schauen, wie sich der Code dann verhält. Um Ihnen die Arbeit zu ersparen, habe ich diese Arbeit für Sie gemacht:

Abbildung 1: Erster Durchgang Abbildung 2: Zweiter Durchgang


Der letzte Abschnitt dieser Funktion zeichnet ein geblendetes Rechteck über den gesamten Bildschirm, um den Schatten zu werfen. Umso dunkler Sie das Rechteck machen, umso dunkler wird der Schatten sein. Um also die Eigenschaften des Schattens zu ändern, ändern Sie das glColor4f Statement. Ein höherer Alpha-Wert wird ihn dunkle machen. Oder Sie können ihn rot, grün, purpur,... machen!

	glFrontFace( GL_CCW );
	glColorMask( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE );	// aktiviere Rendering in den Colour Buffer für alle Komponenten

	// Zeichne ein schattiges Rechteck über den gesamten Bildschirm
	glColor4f( 0.0f, 0.0f, 0.0f, 0.4f );
	glEnable( GL_BLEND );
	glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
	glStencilFunc( GL_NOTEQUAL, 0, 0xFFFFFFFFL );
	glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP );
	glPushMatrix();
	glLoadIdentity();
	glBegin( GL_TRIANGLE_STRIP );
		glVertex3f(-0.1f, 0.1f,-0.10f);
		glVertex3f(-0.1f,-0.1f,-0.10f);
		glVertex3f( 0.1f, 0.1f,-0.10f);
		glVertex3f( 0.1f,-0.1f,-0.10f);
	glEnd();
	glPopMatrix();
	glPopAttrib();
}

Ok, der nächste Teil zeichnet die schattierten Quads. Wie funktioniert das? Was passiert ist, dass Sie durch jede Seite iterieren und wenn diese sichtbar ist, dann überprüfen Sie alle ihre Kanten. Wenn es an der Kante keine benachbarten Seiten gibt oder die benachbarten Seiten nicht sichtbar sind, wirft die Kante einen Schatten. Wenn Sie über die beiden Fälle genau nachdenken, werden Sie bemerken, dass es stimmt. Indem Sie einen Viereck (bestehend aus zwei Dreiecken) zeichnen, bestehend aus den Punkten der Kante und die Kante wird nach hinten durch die Szene projiziert, um den Schattenwurf zu erhalten.

Die "Brute Force" Annäherung, die hier genutzt wird, zeichnet einfach bis "unendlich" und die Schattenpolygone werden einfach an den Polygonen geclippt, auf die sie treffen. Das zieht ein 'Durchdringen' mit sich, was allerdings die Video-Hardware belastet. Für einen High-Performance Änderung an diesem Algorithmus sollten Sie die Polygone an den Objekten dahinter clippen. Das ist viel trickreicher und bringt ein paar Probleme mit sich, aber wenn Sie es so machen wollen, sollten Sie sich diesen Gamasutra Artikel anschauen.

Der Code, der all das macht ist nicht so trickreich, wie es sich anhört. Wir fangen mit einem Ausschnitt an, der durch die Objekte iteriert. Am Ende haben wir eine Kante j und seine benachbarten Seiten, spezifiziert durch neighbourIndex.

void doShadowPass( ShadowedObject& object, GLfloat *lightPosition )
{
	for ( int i = 0; i < object.nFaces; i++ )
	{
		const Face& face = object.pFaces[i];

		if ( face.visible )
		{
			// Go Through Each Edge
			for ( int j = 0; j < 3; j++ )
			{
				int neighbourIndex = face.neighbourIndices[j];

Als nächstes überprüfen Sie, ob es eine sichtbare benachbarte Seite dieses Objektes gibt. Wenn nicht, dann wirft diese Kante einen Schatten.

				// wenn es keinen Nachbarn gibt oder die benachbarte Seite nicht sichtbar ist, dann wirft diese Kante einen Schatten
				if ( neighbourIndex == -1 || object.pFaces[neighbourIndex].visible == false )
				{

Der nächste Codeabschnitt ermittelt die zwei Vertices der aktuellen Kante, v1 und v2. Dann berechnet sie v3 und v4, welche entlängs des Vektors zwischen der Lichtquelle und der ersten Kante projiziert wird. Sie werden auf UNENDLICH (INFINITY) skaliert, was auf einen ziemlich großen Wert gesetzt wurde.

					// ermittle die Punkte auf der Kante
					const Point3f& v1 = object.pVertices[face.vertexIndices[j]];
					const Point3f& v2 = object.pVertices[face.vertexIndices[( j+1 )%3]];

					// berechne den Abstand der beiden Vertices
					Point3f v3, v4;

					v3.x = ( v1.x-lightPosition[0] )*INFINITY;
					v3.y = ( v1.y-lightPosition[1] )*INFINITY;
					v3.z = ( v1.z-lightPosition[2] )*INFINITY;

					v4.x = ( v2.x-lightPosition[0] )*INFINITY;
					v4.y = ( v2.y-lightPosition[1] )*INFINITY;
					v4.z = ( v2.z-lightPosition[2] )*INFINITY;
 

Ich denke Sie verstehen den nächsten Abschnitt, er zeichnet einfach das Viereck, welches durch diese vier Punkte definiert ist:

					// zeichne das Viereck (als Triangle Strip)
					glBegin( GL_TRIANGLE_STRIP );
						glVertex3f( v1.x, v1.y, v1.z );
						glVertex3f( v1.x+v3.x, v1.y+v3.y, v1.z+v3.z );
						glVertex3f( v2.x, v2.y, v2.z );
						glVertex3f( v2.x+v4.x, v2.y+v4.y, v2.z+v4.z );
					glEnd();
				}
			}
		}
	}
}

Damit ist der Schattenwerfen-Abschnitt komplett. Aber wir sind noch nicht fertig! Was ist mit der drawGLScene? Fangen wir mit den einfachen Teilen an: die Buffer löschen, die Lichtquelle positionieren und eine Sphere zeichnen:

bool drawGLScene()
{
	GLmatrix16f Minv;
	GLvector4f wlp, lp;

	// lösche Color Buffer, Depth Buffer, Stencil Buffer
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

	glLoadIdentity();						// Resette Modelview Matrix
	glTranslatef(0.0f, 0.0f, -20.0f);				// Zoom 20 Einheiten in den Bildschirm hinein
	glLightfv(GL_LIGHT1, GL_POSITION, LightPos);			// Positioniere Light1
	glTranslatef(SpherePos[0], SpherePos[1], SpherePos[2]);		// Positioniere die Sphere
	gluSphere(q, 1.5f, 32, 16);					// zeichne eine Sphere

Als nächstes müssen wir die Lichtquelle relativ zum lokalen Koordinatensystem des Objektes berechnen. Die Kommentare erklären jeden Schritt detailiert. Minv speichert die Objekt Transformations Matrix, allerdings in umgekehrter Reihenfolge und mit negativen Argumenten, weshalb es eigentlich die Inverse der Transformations Matrix ist. Dann wird lp als Kopie der Lichtposition erzeugt und mit der Matrix multipliziert. Deshalb ist lp die Lichtposition im Koordinatensystem des Objekts.

	glLoadIdentity();						// Resette Matrix
	glRotatef(-yrot, 0.0f, 1.0f, 0.0f);				// Rotiere um -yrot auf der Y-Achse
	glRotatef(-xrot, 1.0f, 0.0f, 0.0f);				// Rotiere um -xrot auf der X-Achse
	glTranslatef(-ObjPos[0], -ObjPos[1], -ObjPos[2]);		// bewege negativ auf allen Achsen basierend auf den ObjPos[] Werten (X, Y, Z)
	glGetFloatv(GL_MODELVIEW_MATRIX,Minv);				// ermittle ModelView Matrix (speichere in Minv)
	lp[0] = LightPos[0];						// speichere Lichtposition X in lp[0]
	lp[1] = LightPos[1];						// speichere Lichtposition Y in lp[1]
	lp[2] = LightPos[2];						// speichere Lichtposition Z in lp[2]
	lp[3] = LightPos[3];						// speichere Licht-Richtung in lp[3]
	VMatMult(Minv, lp);						// wir speichern den rotierten Lichtvektor im 'lp' Array
 

Nun, etwas Arbeit um den Raum und das Objekt zu zeichnen. Der Aufruf von castShadow zeichnet die Schatten des Objekts.

	glLoadIdentity();						// Resette die Modelview Matrix
	glTranslatef(0.0f, 0.0f, -20.0f);				// Zoome 20 Einheiten in den Bildschirm hinein
	DrawGLRoom();							// zeichne den Raum
	glTranslatef(ObjPos[0], ObjPos[1], ObjPos[2]);			// Positioniere das Objekt
	glRotatef(xrot, 1.0f, 0.0f, 0.0f);				// rotiere auf der X-AChse um xrot
	glRotatef(yrot, 0.0f, 1.0f, 0.0f);				// rotiere auf der Y-Achse um yrot
	drawObject(obj);						// Prozedur um das geladene Objekt zu zeichnen
	castShadow(obj, lp);						// Prozedur um den Schatten basierend auf der Silhouette zu werfen

Die folgende paar Zeilen zeichnen einen kleinen orangenen Kreis, da wo das Licht ist:

	glColor4f(0.7f, 0.4f, 0.0f, 1.0f);				// Setze Farbe auf ein Orange
	glDisable(GL_LIGHTING);						// deaktiviere Beleuchtung
	glDepthMask(GL_FALSE);						// deaktiviere Depth Mask
	glTranslatef(lp[0], lp[1], lp[2]);				// Translatiere zur Licht-Position
									// Beachten Sie, wir sind immer noch im lokalen Koordinaten-System
	gluSphere(q, 0.2f, 16, 8);					// zeichne eine kleine gelbe Sphere (repräsentiert das Licht)
	glEnable(GL_LIGHTING);						// aktiviere Beleuchtung
	glDepthMask(GL_TRUE);						// aktiviere Depth Mask

Der letzte Teil aktuallisiert die Objekt Position und kehrt zurück.

	xrot += xspeed;							// inkrementiere xrot um  xspeed
	yrot += yspeed;							// inkrementiere yrot um yspeed

	glFlush();							// Flushe die OpenGL Pipeline
	return TRUE;							// alles verlief OK
}

Wir haben eine DrawGLRoom Funktion spezifiziert und hier ist sie - viele Rechtecke, auf die Schatten geworfen werden:

void DrawGLRoom()							// Zeichne den Raum (Box)
{
	glBegin(GL_QUADS);						// fange an Quads zu zeichnen
		// Boden
		glNormal3f(0.0f, 1.0f, 0.0f);				// Normalenvektor der nach oben zeigt
		glVertex3f(-10.0f,-10.0f,-20.0f);			// hinten links
		glVertex3f(-10.0f,-10.0f, 20.0f);			// vorne links
		glVertex3f( 10.0f,-10.0f, 20.0f);			// vorne rechts
		glVertex3f( 10.0f,-10.0f,-20.0f);			// hinten rechts
		// Decke
		glNormal3f(0.0f,-1.0f, 0.0f);				// Normalenvektor der nach unten zeigt
		glVertex3f(-10.0f, 10.0f, 20.0f);			// vorne links
		glVertex3f(-10.0f, 10.0f,-20.0f);			// hinten links
		glVertex3f( 10.0f, 10.0f,-20.0f);			// hinten rechts
		glVertex3f( 10.0f, 10.0f, 20.0f);			// vorne rechts
		// vordere Wand
		glNormal3f(0.0f, 0.0f, 1.0f);				// Normalenvektor der weg vom Betrachter zeigt
		glVertex3f(-10.0f, 10.0f,-20.0f);			// oben links
		glVertex3f(-10.0f,-10.0f,-20.0f);			// unten links
		glVertex3f( 10.0f,-10.0f,-20.0f);			// unten rechts
		glVertex3f( 10.0f, 10.0f,-20.0f);			// oben rechts
		// hintere Wand
		glNormal3f(0.0f, 0.0f,-1.0f);				// Normalenvektor der zum Betrachter zeigt
		glVertex3f( 10.0f, 10.0f, 20.0f);			// oben rechts
		glVertex3f( 10.0f,-10.0f, 20.0f);			// unten rechts
		glVertex3f(-10.0f,-10.0f, 20.0f);			// unten links
		glVertex3f(-10.0f, 10.0f, 20.0f);			// oben links
		// linke Wand
		glNormal3f(1.0f, 0.0f, 0.0f);				// Normalenvektor der nach rechts zeigt
		glVertex3f(-10.0f, 10.0f, 20.0f);			// oben vorne
		glVertex3f(-10.0f,-10.0f, 20.0f);			// unten vorne
		glVertex3f(-10.0f,-10.0f,-20.0f);			// unten hinten
		glVertex3f(-10.0f, 10.0f,-20.0f);			// oben hinten
		// rechte Wand
		glNormal3f(-1.0f, 0.0f, 0.0f);				// Normalenvektor der nach links zeigt
		glVertex3f( 10.0f, 10.0f,-20.0f);			// oben hinten
		glVertex3f( 10.0f,-10.0f,-20.0f);			// unten hinten
		glVertex3f( 10.0f,-10.0f, 20.0f);			// unten vorne
		glVertex3f( 10.0f, 10.0f, 20.0f);			// oben vorne
	glEnd();							// fertig mit Zeichnen der Quads
}

Und bevor ich es vergesse, hier ist die VMatMult Funktion welche einen Vektor mit einer Matrix multipliziert (holen Sie das Mathebuch wieder raus!):

void VMatMult(GLmatrix16f M, GLvector4f v)
{
	GLfloat res[4];							// enthält die berechneten Ergebnisse
	res[0]=M[ 0]*v[0]+M[ 4]*v[1]+M[ 8]*v[2]+M[12]*v[3];
	res[1]=M[ 1]*v[0]+M[ 5]*v[1]+M[ 9]*v[2]+M[13]*v[3];
	res[2]=M[ 2]*v[0]+M[ 6]*v[1]+M[10]*v[2]+M[14]*v[3];
	res[3]=M[ 3]*v[0]+M[ 7]*v[1]+M[11]*v[2]+M[15]*v[3];
	v[0]=res[0];							// Ergebnisse werden in v[] gespeichert
	v[1]=res[1];
	v[2]=res[2];
	v[3]=res[3];							// Homogene Koordinate
}

Die Funktion zum Objekt-Laden ist einfach, rufen Sie einfach readObject auf und setzen Sie dann die Verbindungen und die Ebenen-Gleichung für jede Seite.

int InitGLObjects()							// Initialisiere Objekte
{
	if (!readObject("Data/Object2.txt", obj))			// Lese Object2 in obj
	{
		return FALSE;						// wenn fehlgeschlagen, gebe False zurück
	}

	setConnectivity(obj);						// Setze Seite zu Seite Verbindungen

	for ( int i=0;i < obj.nFaces;i++)				// iteriere durch alle Objekt-Seiten
		calculatePlane(obj, obj.pFaces[i]);			// berechne die Ebenen-Gleichung für alle Seiten

	return TRUE;							// gebe True zurück
}

Zu guter Letzt, KillGLObjects ist eine Funktion für die Bequemlichkeit, so dass wenn Sie mehrere Objekte hinzufügen, können Sie sie an einem zentralen Ort hinzufügen.

void KillGLObjects()
{
	killObject( obj );
}

Alle anderen Funktionen benötigen keine weitere Erklärungen. Ich habe den Standard NeHe Tutorial Code ausgelassen, sowie all die Variablen-Definitionen und die Tastatur-Behandlungs-Funktion. Die Kommentare alleine erklären diese genügend.

Einige Dinge die noch zu diesem Tutorial anzumerken sind: Ich muss gesethen, dass es eine langwierige Sache war, dieses Tutorial zu schreiben. Ich hoffe, ihr wisst es zu schätzen, was Jeff hier an Arbeit reinsteckt! Ich hoffe, Sie haben es genossen und ein großer Dank an Banu, der den original Code geschrieben hat! WENN es etwas gibt, was weiterer Erklärung bedarf, können Sie mich (Brett) gerne unterbrettporter@yahoo.com. kontaktieren.

* Randy Ridge fügt hinzu: Um die Schatten auf meiner Karte zu sehen, muss die naha Clipping-Ebene auf 0.001f anstatt von 0.1f im ReSizeGLScene() Code gesetzt werden. Der Code wurde in diesem Tutorial geändert und sollte nun auf allen Karten laufen!